本文将对Linux下的VFS做一个简单介绍,主要包括VFS里面的一些概念,以及文件系统是如何与VFS交互的。
本文所涉及的代码摘自Linux-4.4.0-59
什么是VFS
VFS的全称为virtual File System(虚拟文件系统),可以把它理解为Linux下的文件系统平台。
对应用层来说,只要和VFS打交道就可以了,VFS对外提供了read,write等接口,应用层程序不需要关心底层具体的的文件系统是怎么实现的。
对具体的文件系统来说,VFS就是一个框架,提供了文件系统通用的一些数据结构和函数,文件系统只需要提供VFS所要求的数据和实现所要求的函数就可以了,就像是在VFS上开发一个插件一样。
文件系统在系统中的位置
下面这张图展示了文件系统在系统中的位置
+---------------------------------------------------------------+
| +---------------------+ |
| | User Applications | |
| +---------------------+ |
| | ↓ User Space |
| | +---------------+ |
| | | GNU C Library | |
| | +---------------+ |
|...................|.........|.................................|
| ↓ ↓ |
| +-------------------------+ |
| | System Call Interface | |
| +-------------------------+ |
| ↓ |
| +-----------+ +-------------------------+ +---------------+ |
| |Inode Cache|--| Virtual File System |--|Directory Cache| |
| +-----------+ +-------------------------+ +---------------+ |
| ↓ |
| +-------------------------+ |
| | Individual File System | |
| +-------------------------+ |
| ↓ |
| +----------------+ |
| | Buffer Cache | Kernel Space |
| +----------------+ |
| ↓ |
| +----------------+ |
| | Device Driver | |
| +----------------+ |
+---------------------------------------------------------------+
应用层程序(User Applications)可以通过libc(GNU C Library),或者直接通过内核提供的系统调用(System Call Interface )来访问文件
在内核里面,相应的系统调用里面调用的其实就是VFS提供的函数
VFS根据文件路径找到相应的挂载点,就得到了具体的文件系统信息,然后调用具体文件系统的相应函数。(Inode Cache和Directory Cache是VFS中的一部分,用来加快inode和directory的访问)
具体的文件系统根据自己对磁盘(可能是别的介质,本篇统一以磁盘为例)上数据的组织方式,操作相应的数据。(Buffer Cache是内核中块设备的缓存)
VFS相关的对象
下面这张图简要的说明了VFS里面各种object之间的关系
. . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. Process A . . . . .
. fd . . Kernel . . File System .
. +---+ . . +--------------+ . . .
. stdin | 0 |-------------->| File Object1 |--------+ . . .
. +---+ . . +--------------+ | . . .
. stdout | 1 |-------+ . +--------------+ | . . .
. +---+ . +------>| File Object2 |------+ | . . .
. stderr | 2 |-------+ . +--------------+ | | . . +------------+ .
. +---+ . . +--------------+ | | . +------->|Inode Object|--+ +-----------------+ .
. | 3 |-------------->| File Object3 |----+ | | DEntry Cache . | . +------------+ | +->|Superblock Object| .
. +---+ . . +--------------+ | | | +-------------+ . | . +------------+ | | +-----------------+ .
. |...| . . +--+-+-+->|DEntry Object|------+ +---->|Inode Object|--+ | | .
. +---+ . . | | | +-------------+ . | . +------------+ +--+ | .
. . . . . .. . . . . . | | +--->|DEntry Object|---------+ . +------------+ | | ↓ .
. | | +-------------+ . .+->|Inode Object|--+ | +-----------------+ .
. | +----->|DEntry Object|------------+ +------------+ | | | | .
. . . . . .. . . . . . | | +-------------+ . . +------------+ | +->| Disk | .
. Process B . . | | +-->|DEntry Object|-------------->|Inode Object|--+ | | .
. fd . . +--------------+ | | | +-------------+ . . +------------+ +-----------------+ .
. +---+ . +------>| File Object4 |-+ | | | ..... | . . .
. stdin | 0 |-------+ . +--------------+ | | +-------------+ . . .
. +---+ . . +--------------+ | | . . .
. stdout | 1 |-------------->| File Object5 |----+ | . . .
. +---+ . . +--------------+ | | . . .
. stderr | 2 |-------+ . +--------------+ | | . . .
. +---+ . +------>| File Object6 |----+ | . . .
. | 3 |---+ . . +--------------+ | . . .
. +---+ | . . +--------------+ | . . .
. |...| +---------->| File Object7 |-------+ . . .
. +---+ . . +--------------+ . . .
. . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
fd列表
在进程的眼里,看到的是fd列表,列表里面存放的是指向file object的指针,所以同样的fd在不同的进程中可能会指向不同的file object
File Object
file object是内核中的对象,代表了一个被进程打开的文件,和具体的进程相关联
这里是它的数据结构(只摘取部分)
//linux/fs.h
struct file {
mode_t f_mode; /*rwx权限及文件类型,来自inode object*/
struct path f_path; /*文件的路径,由dentry object组成*/
struct inode *f_inode; /*指向的inode*/
const struct file_operations *f_op; /*和文件相关的函数,比如read,write等,来自inode object*/
loff_t f_pos; /*当前文件访问到的位置*/
}
//linux/path.h
struct path {
struct vfsmount *mnt; /*所属的挂载点信息*/
struct dentry *dentry; /*指向的dentry object*/
};
当应用程序调用open函数的时候,VFS就会创建相应的file object,除了f_pos是进程私有的数据外,其他的数据都来自于inode和dentry,和所有进程共享,不同进程的file object可以指向同一个dentry和inode
DEntry Objectry
DEntry Object由VFS维护,所有文件系统共享,不和具体的进程关联,每个DEntry Object都指向一个Inode object,在加载inode时根据inode自动生成。
当调用open函数打开一个文件时,内核会第一时间到dentry cache里面根据path来找相应的dentry,找到了就直接构造file object并返回,如果没找到的话,就会根据找到的最近的目录一级一级的往下加载,直到找到相应的文件。(加载的过程就是从磁盘加载inode的过程,所有被加载的inode都会生成相应的dentry然后缓存起来)
dentry chache的作用就是用来加快根据path找到相应文件的速度,它里面有自己设计的便于快速查找的数据结构,dentry只存在于内存中,不会被存储在磁盘上,由于内存有限,所以并不是磁盘上所有inode所对应的dentry都会被缓存起来,VFS有自己的缓存策略。
这里是它的数据结构(只摘取部分)
//linux/dcache.h
struct dentry {
struct dentry *d_parent; /* 父目录 */
struct qstr d_name; /* 名字 */
struct inode *d_inode; /* 关联的inode */ */
struct list_head d_child; /* 父目录中的子目录和文件 */
struct list_head d_subdirs; /* 当前目录中的子目录和文件 */
};
注意:dentry虽然名字叫directory entry,实际上它对应的inode也可以是普通文件
Inode object
inode的结构体由VFS定义,代表了磁盘上的一个文件、目录、链接或者其它类型的文件。
inode里面包含的数据存放在磁盘上,由具体的文件系统进行组织,当需要访问一个inode时,会由文件系统从磁盘上加载相应的数据并构造好相应的inode
一个inode可能被多个dentry所关联,比如多个hard links指向同一个文件的情况
这里是它的数据结构(只摘取部分)
//linux/fs.h
struct inode {
umode_t i_mode; /*rwx权限及文件类型*/
kuid_t i_uid; /*user id*/
kgid_t i_gid; /*group id*/
const struct inode_operations *i_op; /*inode相关的操作函数,如创create,mkdir,lookup,rename等,请参考fs.h里的定义*/
struct super_block *i_sb;
unsigned long i_ino; /*inode的编号*/
loff_t i_size; /*文件大小*/
struct timespec i_atime; /*最后访问时间*/
struct timespec i_mtime; /*文件内容最后修改时间*/
struct timespec i_ctime; /*文件元数据最后修改时间(包括文件名称)*/
const struct file_operations *i_fop; /* 文件操作函数,包括open,llseek,read,write等,请参考fs.h里的定义 */
void *i_private; /* 文件系统的私有数据,一般可以从这里面知道文件对应的数据在哪 */
};
Superblock Object
它的结构体由VFS定义,但里面的数据由具体的文件系统填充,每个superblock代表了一个具体的分区。
每个磁盘分区上都有一份superblock,里面包含了当前磁盘分区的信息,如文件系统类型、剩余空间等。
由于superblock非常重要,所以一般文件系统都会在磁盘上存储多份,防止数据损坏导致整个分区无法读取。
这里是它的数据结构(只摘取部分)
//linux/fs.h
struct super_block {
unsigned long s_blocksize; /* block的大小,常见文件系统一般都是4K */
loff_t s_maxbytes; /* 支持的最大文件大小 */
struct file_system_type *s_type; /* 文件系统系统类型 */
struct dentry *s_root; /* 分区内文件树的根节点 */
struct list_head s_mounts; /* 一个分区可以mount到多个地方,这里是mount的相关信息*/
struct block_device *s_bdev; /* 对应的物理设备信息 */
u8 s_uuid[16]; /* 分区的UUID */
void *s_fs_info; /* 文件系统的私有数据*/
};
实现文件系统的大概步骤
文件系统主要负责管理磁盘上的空间,磁盘上至少要包含三部分数据: superblock,inodes和数据块。
首先得有一个创建文件系统的工具(如ext2文件系统的mke2fs),用来将磁盘分区格式化成想要的格式,主要是初始化superblock和root inode。
写一个内核模块,在里面注册自己的文件系统,并且初始化mount函数
当用户在应用层调用mount命令时,VFS就会根据指定的文件系统类型找到我们写的内核模块,并且调用里面的mount函数
在mount函数里面读取磁盘上的superblock和root inode
初始化root inode的inode_operations和file_operations,然后返回给VFS
这样VFS就能根据root inode里提供的函数一级一级的往下找到path对应文件的inode
读取inode所指向的数据块(一个或者多个),根据文件的类型,解析数据块的内容。如果文件类型是普通文件,那么数据块里面就是文件的内容;如果文件类型是目录,那么数据块里面存储的就是目录下面所有子目录和文件的名称及它们对应的inode号;如果文件类型是软链接,那么数据块里面存储的就是链接到的文件路径。
总的来说,实现文件系统就是怎么在磁盘上组织文件,然后实现VFS所要求的superblock,inode以及inode_operations和file_operations。
结束语
本篇只是介绍了VFS里面的基本概念,没有深入细节(因为我也没有相关经验),希望对大家理解文件系统有所帮助。
要实现一个真正的文件系统除了需要了解VFS外,还需要很多内核编程及磁盘操作的知识,如果感兴趣,可以参考ext2文件系统的实现,相对简单且代码少(不到1万行)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。